package client;

/** MainPanel
 *
 * @author pquiring
 *
 * Created : Mar 6, 2017
 */

import java.io.*;
import java.net.*;
import java.awt.*;
import java.awt.event.*;
import javax.swing.*;

import javaforce.*;
import javaforce.media.*;

import common.Config;

public class MainPanel extends javax.swing.JPanel implements MediaIO, ActionListener {

  /**
   * Creates new form MainPanel
   */
  public MainPanel(JFrame frame) {
    initComponents();
    listAudioDevices();
    this.frame = frame;
    mouse = new JFImage();
    mouse.loadPNG(this.getClass().getClassLoader().getResourceAsStream("client/mouse.png"));
    initTray();
    loadServers();
  }

  /**
   * This method is called from within the constructor to initialize the form. WARNING: Do NOT modify this code. The
   * content of this method is always regenerated by the Form Editor.
   */
  @SuppressWarnings("unchecked")
  // <editor-fold defaultstate="collapsed" desc="Generated Code">//GEN-BEGIN:initComponents
  private void initComponents() {

    buttonGroup1 = new javax.swing.ButtonGroup();
    buttonGroup2 = new javax.swing.ButtonGroup();
    audio = new javax.swing.JRadioButton();
    noaudio = new javax.swing.JRadioButton();
    start = new javax.swing.JButton();
    freq = new javax.swing.JComboBox();
    mono = new javax.swing.JRadioButton();
    stereo = new javax.swing.JRadioButton();
    jLabel3 = new javax.swing.JLabel();
    fps = new javax.swing.JSpinner();
    jLabel4 = new javax.swing.JLabel();
    audioDevices = new javax.swing.JComboBox();
    showMouseCursor = new javax.swing.JCheckBox();
    jLabel5 = new javax.swing.JLabel();
    vBitRate = new javax.swing.JComboBox();
    jLabel6 = new javax.swing.JLabel();
    aBitRate = new javax.swing.JComboBox();
    jLabel7 = new javax.swing.JLabel();
    jLabel8 = new javax.swing.JLabel();
    jLabel1 = new javax.swing.JLabel();
    jLabel2 = new javax.swing.JLabel();
    jLabel9 = new javax.swing.JLabel();
    serverField = new javax.swing.JComboBox<>();

    buttonGroup1.add(audio);
    audio.setSelected(true);
    audio.setText("Audio");

    buttonGroup1.add(noaudio);
    noaudio.setText("No Audio");

    start.setText("Start");
    start.addActionListener(new java.awt.event.ActionListener() {
      public void actionPerformed(java.awt.event.ActionEvent evt) {
        startActionPerformed(evt);
      }
    });

    freq.setModel(new javax.swing.DefaultComboBoxModel(new String[] { "44100", "22050", "11025", "8000" }));
    freq.addActionListener(new java.awt.event.ActionListener() {
      public void actionPerformed(java.awt.event.ActionEvent evt) {
        freqActionPerformed(evt);
      }
    });

    buttonGroup2.add(mono);
    mono.setSelected(true);
    mono.setText("mono");

    buttonGroup2.add(stereo);
    stereo.setText("stereo");

    jLabel3.setText("FPS");

    fps.setModel(new javax.swing.SpinnerNumberModel(16, 1, 60, 1));

    jLabel4.setText("Audio Device");

    showMouseCursor.setSelected(true);
    showMouseCursor.setText("Show mouse cursor");

    jLabel5.setText("Video Quality");

    vBitRate.setModel(new javax.swing.DefaultComboBoxModel(new String[] { "8M", "6M", "4M", "2M", "1M", "800k", "400k", "200k" }));
    vBitRate.setSelectedIndex(4);
    vBitRate.setToolTipText("");
    vBitRate.addActionListener(new java.awt.event.ActionListener() {
      public void actionPerformed(java.awt.event.ActionEvent evt) {
        vBitRateActionPerformed(evt);
      }
    });

    jLabel6.setText("Audio Quality");

    aBitRate.setModel(new javax.swing.DefaultComboBoxModel(new String[] { "256k", "192k", "128k", "64k", "32k" }));
    aBitRate.setSelectedIndex(2);
    aBitRate.setToolTipText("");

    jLabel7.setText("Freq:");

    jLabel8.setText("When you start a tray icon you be added to pause/stop projector.");

    jLabel1.setText("bits/sec");

    jLabel2.setText("bits/sec");

    jLabel9.setText("Server");

    serverField.setEditable(true);

    javax.swing.GroupLayout layout = new javax.swing.GroupLayout(this);
    this.setLayout(layout);
    layout.setHorizontalGroup(
      layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
      .addGroup(layout.createSequentialGroup()
        .addContainerGap()
        .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
          .addGroup(layout.createSequentialGroup()
            .addGap(0, 0, Short.MAX_VALUE)
            .addComponent(start))
          .addGroup(layout.createSequentialGroup()
            .addComponent(jLabel9)
            .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED)
            .addComponent(serverField, 0, javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE))
          .addGroup(layout.createSequentialGroup()
            .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
              .addGroup(layout.createSequentialGroup()
                .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
                  .addGroup(layout.createSequentialGroup()
                    .addComponent(audio)
                    .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.UNRELATED)
                    .addComponent(jLabel7))
                  .addComponent(noaudio))
                .addComponent(freq, javax.swing.GroupLayout.PREFERRED_SIZE, 93, javax.swing.GroupLayout.PREFERRED_SIZE)
                .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED)
                .addComponent(mono)
                .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.UNRELATED)
                .addComponent(stereo))
              .addGroup(layout.createSequentialGroup()
                .addComponent(jLabel3)
                .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED)
                .addComponent(fps, javax.swing.GroupLayout.PREFERRED_SIZE, 56, javax.swing.GroupLayout.PREFERRED_SIZE))
              .addGroup(layout.createSequentialGroup()
                .addComponent(jLabel5)
                .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED)
                .addComponent(vBitRate, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE)
                .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED)
                .addComponent(jLabel1))
              .addComponent(showMouseCursor)
              .addGroup(layout.createSequentialGroup()
                .addComponent(jLabel6)
                .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED)
                .addComponent(aBitRate, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE)
                .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED)
                .addComponent(jLabel2))
              .addComponent(jLabel8))
            .addGap(0, 0, Short.MAX_VALUE))
          .addGroup(layout.createSequentialGroup()
            .addComponent(jLabel4)
            .addGap(23, 23, 23)
            .addComponent(audioDevices, 0, javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE)))
        .addContainerGap())
    );
    layout.setVerticalGroup(
      layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
      .addGroup(layout.createSequentialGroup()
        .addContainerGap()
        .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.BASELINE)
          .addComponent(jLabel9)
          .addComponent(serverField, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE))
        .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED)
        .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.BASELINE)
          .addComponent(jLabel4)
          .addComponent(audioDevices, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE))
        .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.UNRELATED)
        .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.BASELINE)
          .addComponent(fps, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE)
          .addComponent(jLabel3))
        .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.UNRELATED)
        .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.BASELINE)
          .addComponent(audio)
          .addComponent(freq, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE)
          .addComponent(mono)
          .addComponent(stereo)
          .addComponent(jLabel7))
        .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.UNRELATED)
        .addComponent(noaudio)
        .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED)
        .addComponent(showMouseCursor)
        .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED)
        .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.BASELINE)
          .addComponent(jLabel5)
          .addComponent(vBitRate, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE)
          .addComponent(jLabel1))
        .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED)
        .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.BASELINE)
          .addComponent(jLabel6)
          .addComponent(aBitRate, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE)
          .addComponent(jLabel2))
        .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED)
        .addComponent(jLabel8)
        .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED, javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE)
        .addComponent(start)
        .addContainerGap())
    );
  }// </editor-fold>//GEN-END:initComponents

  private void startActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_startActionPerformed
    start();
  }//GEN-LAST:event_startActionPerformed

  private void freqActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_freqActionPerformed
    // TODO add your handling code here:
  }//GEN-LAST:event_freqActionPerformed

  private void vBitRateActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_vBitRateActionPerformed
    // TODO add your handling code here:
  }//GEN-LAST:event_vBitRateActionPerformed

  // Variables declaration - do not modify//GEN-BEGIN:variables
  private javax.swing.JComboBox aBitRate;
  private javax.swing.JRadioButton audio;
  private javax.swing.JComboBox audioDevices;
  private javax.swing.ButtonGroup buttonGroup1;
  private javax.swing.ButtonGroup buttonGroup2;
  private javax.swing.JSpinner fps;
  private javax.swing.JComboBox freq;
  private javax.swing.JLabel jLabel1;
  private javax.swing.JLabel jLabel2;
  private javax.swing.JLabel jLabel3;
  private javax.swing.JLabel jLabel4;
  private javax.swing.JLabel jLabel5;
  private javax.swing.JLabel jLabel6;
  private javax.swing.JLabel jLabel7;
  private javax.swing.JLabel jLabel8;
  private javax.swing.JLabel jLabel9;
  private javax.swing.JRadioButton mono;
  private javax.swing.JRadioButton noaudio;
  private javax.swing.JComboBox<String> serverField;
  private javax.swing.JCheckBox showMouseCursor;
  private javax.swing.JButton start;
  private javax.swing.JRadioButton stereo;
  private javax.swing.JComboBox vBitRate;
  // End of variables declaration//GEN-END:variables

  private AudioInput mic;
  private boolean active = false;
  private boolean paused = false;
  private boolean working = false;
  private JFrame frame;
  private JFImage mouse;
  private Socket socket;
  private OutputStream os;

  public void listAudioDevices() {
    mic = new AudioInput();
    String list[] = mic.listDevices();
    audioDevices.removeAllItems();
    for(int a=0;a<list.length;a++) {
      audioDevices.addItem(list[a]);
    }
  }

  public void setState(boolean state) {
    start.setEnabled(state);
    if (state) start.setText("Start");
    audioDevices.setEnabled(state);
    audio.setEnabled(state);
    noaudio.setEnabled(state);
    mono.setEnabled(state);
    stereo.setEnabled(state);
    freq.setEnabled(state);
    fps.setEnabled(state);
  }

  public void start() {
    if (working) {
      active = false;
      start.setText("Stopping");
      return;
    }
    saveServers();
    working = true;
    setState(false);
    new Worker().start();
  }

  public void failed(String msg) {
    JFAWT.showError("Error", msg);
    setState(true);
    working = false;
  }

  public void swapEndian(byte in[], short out[]) {
    int p = 0;
    short s;
    int cnt = out.length;
    for(int a=0;a<cnt;a++) {
      s = in[p++];
      s <<= 8;
      s += in[p++] & 0xff;
      out[a] = s;
    }
  }

  public class Worker extends Thread {
    public void run() {
      setState(false);

      boolean doAudio = audio.isSelected();
      int audioRate = JF.atoi((String)freq.getSelectedItem());
      if (audioRate < 8000 || audioRate > 44100) audioRate = 44100;
      int chs = mono.isSelected() ? 1 : 2;
      int frameRate = (Integer)fps.getValue();
      int samples = audioRate * chs / frameRate;
      try {
        socket = new Socket((String)serverField.getSelectedItem(), Config.port);
        os = socket.getOutputStream();
      } catch (Exception e) {
        JFLog.log(e);
        failed("Unable to connect to server");
        return;
      }

      JFImage img = JFImage.createScreenCapture();

      MediaEncoder encoder = new MediaEncoder();
      encoder.setAudioBitRate(getAudioBitRate());
      encoder.setVideoBitRate(getVideoBitRate());
      int width = img.getWidth();
      int height = img.getHeight();
      JFLog.log("size=" + width + "," + height);
      JFLog.log("frameRate=" + frameRate);
      JFLog.log("audioRate=" + audioRate + ",chs=" + chs);
      if (!encoder.start(MainPanel.this, width, height, frameRate, chs
        , audioRate, "h264", true, doAudio))
      {
        failed("Unable to create output file");
        return;
      }
      if (doAudio) {
        mic = new AudioInput();
        if (!mic.start(chs, audioRate, 16, samples * 2, (String)audioDevices.getSelectedItem())) {
          failed("Unable to start recording audio");
          return;
        }
      }

      active = true;
      paused = false;
      start.setText("Stop");
      start.setEnabled(true);
      byte sams8[] = new byte[samples*2];
      short sams16[] = new short[samples];

      double current = System.currentTimeMillis();
      double delay = 1000.0 / frameRate;

      boolean showMouse = showMouseCursor.isSelected();

      if (frame != null) frame.setVisible(false);
      boolean skip_frame = false;
      pause.setLabel("Pause");
      addTray();
      while (active) {
        if (paused) {
          while (mic.read(sams8)) {}  //discard audio
          JF.sleep(100);
          continue;
        }
        if (!skip_frame) {
          img = JFImage.createScreenCapture();
          if (showMouse) {
            Point mpt = MouseInfo.getPointerInfo().getLocation();
            img.putJFImageBlend(mouse, mpt.x, mpt.y, true);
          }
        } else {
          skip_frame = false;
        }
        int px[] = img.getPixels();
        encoder.addVideo(px);
        if (doAudio) {
          while (mic.read(sams8)) {
            swapEndian(sams8, sams16);
            encoder.addAudio(sams16);
          }
        }
        double now = System.currentTimeMillis();
        int sleep = (int)(delay - (now - current));
        if (sleep > 0) {
          JF.sleep(sleep);
        } else {
          skip_frame = true;  //system too slow
        }
        current += delay;
      }
      delTray();
      if (frame != null) frame.setVisible(true);
      encoder.stop();
      if (doAudio) mic.stop();
      try {
        socket.close();
      } catch (Exception e) {
        JFLog.log(e);
      }
      working = false;
      setState(true);
    }
  }

  public int read(MediaCoder coder, byte[] bytes) {
    return 0;
  }

  public int write(MediaCoder coder, byte[] bytes) {
    try {
      os.write(bytes);
      return bytes.length;
    } catch (Exception e) {
      JFLog.log(e);
      active = false;
      return 0;
    }
  }

  public long seek(MediaCoder coder, long pos, int how) {
/*
    try {
      switch (how) {
        case MediaCoder.SEEK_SET: break;
        case MediaCoder.SEEK_CUR: pos += raf.getFilePointer(); break;
        case MediaCoder.SEEK_END: pos += raf.length(); break;
      }
      raf.seek(pos);
      return pos;
    } catch (Exception e) {
      JFLog.log(e);
    }
*/
    return 0;
  }

  public SystemTray tray;
  public TrayIcon icon;
  public MenuItem stop, pause;

  private void initTray() {
    if (SystemTray.isSupported()) {
      tray = SystemTray.getSystemTray();
      // create a popup menu
      PopupMenu popup = new PopupMenu();
      pause = new MenuItem("Pause");
      pause.addActionListener(this);
      popup.add(pause);
      popup.addSeparator();
      stop = new MenuItem("Stop");
      stop.addActionListener(this);
      popup.add(stop);
      Dimension size = tray.getTrayIconSize();
      JFImage appicon = new JFImage();
      InputStream is = this.getClass().getResourceAsStream("/jfprojector.png");
      appicon.loadPNG(is);
      try {is.close();} catch (Exception e) {}
      JFImage scaled = new JFImage(size.width, size.height);
      scaled.fill(0, 0, size.width, size.height, 0x00000000, true);  //fill with alpha transparent
      if (true) {
        //scaled image (looks bad sometimes)
        scaled.getGraphics().drawImage(appicon.getImage()
          , 0, 0, size.width, size.height
          , 0, 0, appicon.getIconWidth(), appicon.getIconHeight()
          , null);
      } else {
        //center image
        scaled.getGraphics().drawImage(appicon.getImage()
          , (size.width - appicon.getIconWidth()) / 2
          , (size.height - appicon.getIconHeight()) / 2
          , null);
      }
      icon = new TrayIcon(scaled.getImage(), "jfProjector", popup);
      icon.addActionListener(this);
    }
  }

  private void addTray() {
    try { tray.add(icon); } catch (Exception e) { JFLog.log(e); }
  }

  private void delTray() {
    try { tray.remove(icon); } catch (Exception e) { JFLog.log(e); }
  }

  public void actionPerformed(ActionEvent e) {
    Object o = e.getSource();
    if (o == stop) {
      active = false;
    }
    if (o == pause) {
      paused = !paused;
      if (paused) {
        pause.setLabel("Unpause");
      } else {
        pause.setLabel("Pause");
      }
    }
  }

  private int getRate(String rate) {
    int scale = 1;
    if (rate.endsWith("M")) {
      rate = rate.substring(0, rate.length()-1);
      scale = 1024 * 1024;
    }
    else if (rate.endsWith("k")) {
      rate = rate.substring(0, rate.length()-1);
      scale = 1024;
    }
    int value = JF.atoi(rate) * scale;
    System.out.println("rate=" + value);
    return value;
  }

  private int getAudioBitRate() {
    return getRate((String)aBitRate.getSelectedItem());
  }

  private int getVideoBitRate() {
    return getRate((String)vBitRate.getSelectedItem());
  }

  private void loadServers() {
    try {
      BufferedReader br = new BufferedReader(new FileReader(JF.getUserPath() + "/jfprojector.ini"));
      String ln;
      while ((ln = br.readLine()) != null) {
        serverField.addItem(ln);
      }
    } catch (FileNotFoundException fnf) {

    } catch (Exception e) {
      JFLog.log(e);
    }
  }

  private void saveServers() {
    try {
      StringBuffer sb = new StringBuffer();
      int cnt = serverField.getItemCount();
      String svr = (String)serverField.getSelectedItem();
      for(int a=0;a<cnt;a++) {
        String item = serverField.getItemAt(a);
        if (item.equals(svr)) {
          svr = null;
        }
        sb.append(item);
        sb.append("\r\n");
      }
      if (svr != null) {
        sb.append(svr);
        sb.append("\r\n");
      }
      FileOutputStream fos = new FileOutputStream(JF.getUserPath() + "/jfprojector.ini");
      fos.write(sb.toString().getBytes());
      fos.close();
    } catch (Exception e) {
      JFLog.log(e);
    }
  }
}
